use std::io::Write;

use anyhow::Result;
use chrono::Local;
use fudata::db;
use fudata::db::todo;
use fudata::model;
use log::trace;
use surrealdb::sql::Thing;
use crate::index::indexes_sh_craw;
use crate::index::indexes_sz_craw;
use crate::index::indexes_zzzs_craw;
use crate::todo::MAX_IDX_SECURITY;
use fudata::model::kline::Klt;
use fudata::model::todo::Todo;
use fudata::model::todo::TodoParameter;
use fudata::model::todo::TodoStatus;
use fudata::model::todo::TodoType;
use log::error;
use log::info;
use std::cmp::max;

use crate::helper;
use crate::index::index_kline_craw;
use crate::todo::EASYMONEY_KLTS;
use crate::todo::MAX_KLINE_DAYS;

///
/// every day we update the index list after 15:30
///
pub async fn generate_todo_crawl_index_list() -> Result<()> {
    let start = Local::now();
    info!("generate_todo_crawl_index_list start...");
    let todo_id = Todo::id_from(&[TodoType::CrawlIndexList.to_string().as_str()]);

    let todo_rs = todo::find(todo_id.as_str()).await;
    match &todo_rs {
        Ok(v) =>
            match v {
                Some(todo) => {
                    // if there is pending todo, then let it be or else update the plan_at to next 1 midnight
                    if todo.is_done() || todo.is_failed() {
                        let plan_at = helper::now_plus_date_time(1, 0, 0, 0);
                        let todo_update = Todo {
                            plan_at: Some(plan_at),
                            status: Some(TodoStatus::Pending),
                            ..Default::default()
                        };
                        let _ = todo::upsert(todo_id.as_str(), &todo_update).await?;
                    }
                }
                None => {
                    // create a new todo, today midnight
                    let plan_at = helper::now_plus_date_time(0, 0, 0, 0);
                    let todo_new = Todo {
                        name: Some("刷新沪深中证指数列表".to_string()),
                        type_: Some(TodoType::CrawlIndexList),
                        // updatable fields
                        plan_at: Some(plan_at),
                        status: Some(TodoStatus::Pending),
                        created_at: Some(Local::now()),
                        ..Default::default()
                    };
                    let _ = todo::upsert(todo_id.as_str(), &todo_new).await?;
                }
            }
        Err(e) => error!("{}", e),
    }
    trace!(
        "generate_todo_crawl_index_list end/ spent seconds={}",
        Local::now().signed_duration_since(start).num_seconds()
    );

    Ok(())
}

///
/// every day we update the index klines for all index securities
///
/// if exist klines, just do incremental update, or else clean the klines from the db
///
pub async fn generate_todos_crawl_index_klines(max_days: i64) -> Result<()> {
    let start = Local::now();

    info!("generate_todos_crawl_index_klines start...");

    let rs = db::security::select(
        &None,
        Some(model::security::SecurityType::IDX),
        None,
        None,
        0,
        MAX_IDX_SECURITY
    ).await;

    let mut todos = Vec::new();

    match rs {
        Ok(securities) => {
            info!(
                "generate_todos_crawl_index_klines for {} securities's klt: {:?} ...",
                securities.len(),
                EASYMONEY_KLTS
            );

            let mut i = 0;
            for security in securities {
                let security_id = &security.id_raw();
                for klt in EASYMONEY_KLTS {
                    let rs_p = get_todo_update_index_klines_parameters(
                        security_id,
                        &klt,
                        max_days
                    ).await;
                    match rs_p {
                        Ok(p) => {
                            let todo_id = Todo::id_from(&[security_id, klt.to_string().as_str()]);

                            let todo_rs = todo::find(todo_id.as_str()).await;
                            match todo_rs {
                                Ok(v) =>
                                    match v {
                                        Some(todo) => {
                                            // if there is pending todo, then let it be or else update the plan_at to tomorrow
                                            if todo.is_done() || todo.is_failed() {
                                                let todo_update = Todo {
                                                    id: Some(
                                                        Thing::from(("todo", todo_id.as_str()))
                                                    ),
                                                    // updatable fields
                                                    plan_at: Some(
                                                        helper::now_plus_date_time(0, 0, 10, 0)
                                                    ),
                                                    status: Some(TodoStatus::Pending),
                                                    parameters: Some(p),
                                                    ..Default::default()
                                                };

                                                todos.push(todo_update);
                                            }
                                        }
                                        None => {
                                            let todo_new = Todo {
                                                id: Some(Thing::from(("todo", todo_id.as_str()))),
                                                name: Some(
                                                    format!("刷新指数{}的{}K线", security_id, &klt)
                                                ),
                                                type_: Some(TodoType::CrawlIndexKlines),
                                                // updatable fields
                                                plan_at: Some(
                                                    helper::now_plus_date_time(0, 0, 0, 0)
                                                ),
                                                status: Some(TodoStatus::Pending),
                                                parameters: Some(p),
                                                created_at: Some(Local::now()),
                                                ..Default::default()
                                            };
                                            todos.push(todo_new);
                                        }
                                    }
                                Err(e) => error!("{}", e),
                            }
                        }
                        Err(e) => error!("{}", e),
                    }
                    if i % 100 == 0 {
                        print!(
                            "\n~~(crawl_index_todo::generate_todos_crawl_index_klines[{:04}])~~",
                            i
                        );
                    }
                    print!("{}", i % 10);
                    let _rs = std::io::stdout().flush(); // Flush stdout to ensure immediate output
                    i += 1;
                }
            }
        }
        Err(e) => error!("{}", e),
    }

    let _todo_rs = db::todo::upsert_all(&todos).await;

    info!(
        "generate_todos_crawl_index_klines end/ spent seconds={} _todo_rs={:?} ",
        Local::now().signed_duration_since(start).num_seconds(),
        _todo_rs
    );
    Ok(())
}

async fn get_todo_update_index_klines_parameters(
    security_id: &str,
    klt: &Klt,
    max_days: i64
) -> Result<Vec<TodoParameter>> {
    let rs_kl = db::kline::select(security_id, None, Some(&klt), Some("klts DESC"), 0, 1).await;

    match rs_kl {
        Ok(vks) => {
            let mut limit = MAX_KLINE_DAYS;
            // if exist klines, just do incremental update
            if vks.len() > 0 {
                if klt == &Klt::DAY || klt == &Klt::WEEK || klt == &Klt::MONTH {
                    limit = max_days;
                } else {
                    limit = max(1000i64, max_days);
                }
            }

            return Ok(
                [
                    TodoParameter {
                        name: "security_id".to_string(),
                        value: security_id.to_string(),
                    },
                    TodoParameter {
                        name: "klt".to_string(),
                        value: klt.to_string(),
                    },
                    TodoParameter {
                        name: "limit".to_string(),
                        value: limit.to_string(),
                    },
                ].to_vec()
            );
        }
        Err(e) => {
            return Err(e);
        }
    }
}

pub async fn crawl_index_list(worker_no: u8, todo: &Todo) {
    let rs1 = indexes_zzzs_craw::update_indexes_zzzs_list_all_from_eastmoney().await;
    let rs2 = indexes_sh_craw::update_indexes_sh_list_all_from_eastmoney().await;
    let rs3 = indexes_sz_craw::update_indexes_sz_list_all_from_eastmoney().await;
    let rs = match (rs1, rs2, rs3) {
        (Ok(_), Ok(_), Ok(_)) => Ok(()),
        (Ok(_), Ok(_), Err(e3)) => Err(e3),
        (Ok(_), Err(e2), Ok(_)) => Err(e2),
        (Ok(_), Err(e2), Err(e3)) => Err(e2.context(e3)),
        (Err(e1), Ok(_), Ok(_)) => Err(e1),
        (Err(e1), Ok(_), Err(e3)) => Err(e1.context(e3)),
        (Err(e1), Err(e2), Ok(_)) => Err(e1.context(e2)),
        (Err(e1), Err(e2), Err(e3)) => Err(e1.context(e2).context(e3)),
    };
    let _ = super::update_todo_with_result(worker_no, rs, todo).await;
}

pub async fn crawl_index_klines(worker_no: u8, todo: &Todo) {
    let security = db::todo::get_security_by_parameter(todo).await;
    let klt = db::todo::get_klt_by_parameter(todo);
    let limit = db::todo::get_limit_by_parameter(todo);

    let rs = match (security, klt, limit) {
        (Ok(s), Ok(k), Ok(lmt)) => {
            let rs = index_kline_craw::update_index_kline_from_eastmoney(
                &s,
                "20300101",
                &k,
                1,
                lmt
            ).await;
            rs
        }
        (Err(e1), Err(e2), Err(e3)) => Err(e1.context(e2).context(e3)),
        (Ok(_), Ok(_), Err(e3)) => Err(e3),
        (Ok(_), Err(e2), Ok(_)) => Err(e2),
        (Ok(_), Err(e2), Err(e3)) => Err(e2.context(e3)),
        (Err(e1), Ok(_), Ok(_)) => Err(e1),
        (Err(e1), Ok(_), Err(e3)) => Err(e1.context(e3)),
        (Err(e1), Err(e2), Ok(_)) => Err(e1.context(e2)),
    };

    let _ = super::update_todo_with_result(worker_no, rs, todo).await;
}

#[cfg(test)]
mod test {
    use fudata::db;
    use fudata::model::todo;
    use fudata::model::todo::Todo;

    use crate::todo::crawl_index_todo;

    #[tokio::test]
    async fn test_generate_todo_crawl_index_list() {
        pretty_env_logger::init_timed();
        let _ = fudata::db::init().await.unwrap();

        let _ = crawl_index_todo::generate_todo_crawl_index_list().await;

        let todo_id = Todo::id_from(&[todo::TodoType::CrawlIndexList.to_string().as_str()]);

        let rs = db::todo::find(todo_id.as_str()).await;
        println!("rs: {:?}", rs);
    }

    #[tokio::test]
    async fn test_generate_todos_crawl_index_klines() {
        pretty_env_logger::init_timed();
        let _ = fudata::db::init().await.unwrap();

        let _ = crawl_index_todo::generate_todos_crawl_index_klines(10).await;

        let rs = db::todo::select(
            &Some(vec![todo::TodoType::CrawlIndexKlines]),
            &Some(todo::TodoStatus::Pending),
            false,
            0,
            10
        ).await;
        println!("rs: {:?}", rs);
    }
}
